﻿// <copyright file="Transiever.cs" company="Hallvard Korsgaard">
//  Microsoft Public License (Ms-PL). See license details here: http://enoceannet.codeplex.com/license
// </copyright>
// <author>Hallvard Korsgaard</author>

namespace StcEthernet
{
  using System;
  using System.Collections.Concurrent;
  using System.Globalization;
  using System.Net.Sockets;
  using System.Text;
  using System.Threading;
  using EnOcean;
  using EnOcean.Command;
  using EnOcean.Message;
  using EnOcean.Radio.Modem;
  using EnOcean.Radio.Sensor;
  using StcEthernet.Management;

  /// <summary>
  /// The Transiever class represenst the server side of a connected transiever
  /// Use this class to send and receieve messages from a transiever.
  /// </summary>
  public class Transiever
  {
    /// <summary>
    /// Buffer for incomming messages.
    /// </summary>
    private byte[] receiveBuffer;

    /// <summary>
    /// Buffer for outgoing messages.
    /// </summary>
    private byte[] sendBuffer;

    /// <summary>
    /// Buffer for management messages.
    /// </summary>
    private byte[] managementSendBuffer;

    /// <summary>
    /// This wait handle is used for waiting for a response when sending a message.
    /// </summary>
    private ManualResetEvent responseReceived;

    /// <summary>
    /// The queue of outgoing messages.
    /// </summary>
    private BlockingCollection<SendTelegram> sendQueue;

    /// <summary>
    /// The socket that the transiever is connected to.
    /// </summary>
    private Socket client;

    /// <summary>
    /// Indicates whether the BaseId property has been set or not.
    /// </summary>
    private bool isBaseIdSet;

    /// <summary>
    /// Indicates whether the IsModemOn property has been set or not.
    /// </summary>
    private bool isModemOnSet;

    /// <summary>
    /// Indicates whether the ModemId property has been set or not.
    /// </summary>
    private bool isModemIdSet;

    /// <summary>
    /// Indicates whether the RadioSensitivity property has been set or not.
    /// </summary>
    private bool isRadioSensitivitySet;

    /// <summary>
    /// Indicates whether the SoftwareVersion property has been set or not.
    /// </summary>
    private bool isSoftwareVersionSet;

    /// <summary>
    /// Indicates whether the OperatingMode property has been set or not.
    /// </summary>
    private bool isOperatingModeSet;

    /// <summary>
    /// Indicates whether the Initialized event has been triggerd or not.
    /// </summary>
    private bool hasInitializedEventFired;

    /// <summary>
    /// Variable for the BaseId property.
    /// </summary>
    private long baseId;

    /// <summary>
    /// Variable for the IsModemOn property.
    /// </summary>
    private bool isModemOn;

    /// <summary>
    /// Variable for the ModemId property.
    /// </summary>
    private int modemId;

    /// <summary>
    /// Variable for the RadioSensitivity property.
    /// </summary>
    private RadioSensitivity radioSensitivity;

    /// <summary>
    /// Variable for the SoftwareVersion property.
    /// </summary>
    private string softwareVersion;

    /// <summary>
    /// Variable for the OperatingMode property.
    /// </summary>
    private OperatingMode operatingMode;

    /// <summary>
    /// Initializes a new instance of the Transiever class.
    /// </summary>
    /// <param name="client">A connected socket to communicate with the transiever.</param>
    internal Transiever(Socket client)
    {
      this.receiveBuffer = new byte[Constants.ReceiveBufferSize];
      this.sendBuffer = new byte[Constants.SendBufferSize];
      this.managementSendBuffer = new byte[Constants.ManagementSendBufferSize];
      this.sendQueue = new BlockingCollection<SendTelegram>();
      this.responseReceived = new ManualResetEvent(true);
      this.client = client;
    }

    /// <summary>
    /// The event fires when a message is received from the transiever.
    /// </summary>
    public event EventHandler<TelegramReceivedEventArgs> TelegramReceived = delegate { };

    /// <summary>
    /// The event fires when a management telegram is received from the transiever.
    /// </summary>
    public event EventHandler<ManagementTelegramReceivedEventArgs> ManagementTelegramReceived = delegate { };

    /// <summary>
    /// The event fires if an expected response message is not received when sending a message.
    /// </summary>
    public event EventHandler<ResponseTimeoutEventArgs> ResponseTimeout = delegate { };

    /// <summary>
    /// The event fires if a message that could not be interpreted arrives.
    /// </summary>
    public event EventHandler<UnknownDataReceivedEventArgs> UnknownTelegramReceived = delegate { };

    /// <summary>
    /// The event fires when a transiever (or receiver) disconnects.
    /// </summary>
    public event EventHandler<TransieverDisconnectedEventArgs> TransieverDisconnected = delegate { };

    /// <summary>
    /// The event fires when all properties has been set by the transiever.
    /// </summary>
    internal event EventHandler<EventArgs> Initialized = delegate { };

    /// <summary>
    /// Gets the base id of the transiever.
    /// This property is updated when an InformIdBase message is received from
    /// the transiever. Before such a message is received this property returns 0.
    /// </summary>
    public long BaseId
    {
      get
      {
        return this.baseId;
      }

      private set
      {
        this.baseId = value;
        this.isBaseIdSet = true;
      }
    }

    /// <summary>
    /// Gets a value indicating whether the modem is on or off. True = On, false = Off.
    /// This property is updated when an InformModemStatus message is received from
    /// the transiever. Before such a message is received this property returns false.
    /// </summary>
    public bool IsModemOn
    {
      get
      {
        return this.isModemOn;
      }

      private set
      {
        this.isModemOn = value;
        this.isModemOnSet = true;
      }
    }

    /// <summary>
    /// Gets the 16 bit modem ID.
    /// This property is updated when an InformModemStatus message is received from
    /// the transiever. Before such a message is received this property returns false.
    /// </summary>
    public int ModemId
    {
      get
      {
        return this.modemId;
      }

      private set
      {
        this.modemId = value;
        this.isModemIdSet = true;
      }
    }

    /// <summary>
    ///  Gets a value indicating whether the properties has been updated from the transiever or not.
    ///  When this property is false the following properties return ivalid data: BaseId, IsModemSet, ModemOn, OperatingMode, RadioSensitivity and SoftwareVersion.
    /// </summary>
    public bool ArePropertiesInitialized
    {
      get
      {
        return this.isBaseIdSet & this.isModemIdSet & this.isModemOnSet & this.isOperatingModeSet & this.isRadioSensitivitySet & this.isSoftwareVersionSet;
      }
    }

    /// <summary>
    /// Gets the RadioSensitivity.
    /// This property is updated when an InformRadioSensitivity message is received from
    /// the transiever. Before such a message is received this property returns Low.
    /// </summary>
    public RadioSensitivity RadioSensitivity
    {
      get
      {
        return this.radioSensitivity;
      }

      private set
      {
        this.radioSensitivity = value;
        this.isRadioSensitivitySet = true;
      }
    }

    /// <summary>
    /// Gets the transiever software version.
    /// This property is updated when an InformSoftwareVersion message is received from
    /// the transiever. Before such a message is received this property returns null.
    /// </summary>
    public string SoftwareVersion
    {
      get
      {
        return this.softwareVersion;
      }

      private set
      {
        this.softwareVersion = value;
        this.isSoftwareVersionSet = true;
      }
    }

    /// <summary>
    /// Gets the operating mode of the transiever.
    /// </summary>
    public OperatingMode OperatingMode
    {
      get
      {
        return this.operatingMode;
      }

      private set
      {
        this.operatingMode = value;
        this.isOperatingModeSet = true;
      }
    }

    /// <summary>
    /// Sends a SensorTelegram to the transiever. This method is asynchronous and
    /// returns immediately.
    /// </summary>
    /// <param name="telegram">The SensorTelegram to send.</param>
    public void Send(SensorTelegram telegram)
    {
      this.sendQueue.Add(new SendEnOceanTelegram(telegram));
    }

    /// <summary>
    /// Sends a CommandTelegram to the transiever. This method is asynchronous and
    /// returns immediately.
    /// </summary>
    /// <param name="telegram">The CommandTelegram to send.</param>
    public void Send(CommandTelegram telegram)
    {
      this.sendQueue.Add(new SendEnOceanTelegram(telegram));
    }

    /// <summary>
    /// Sends a ModemTelegram to the transiever. This method is asynchronous and
    /// returns immediately.
    /// </summary>
    /// <param name="telegram">The ModemTelegram to send.</param>
    public void Send(ModemTelegram telegram)
    {
      this.sendQueue.Add(new SendEnOceanTelegram(telegram));
    }

    /// <summary>
    /// Sends a ManagementTelegram to the transiever. This method is asynchronous and
    /// returns immediately.
    /// </summary>
    /// <param name="telegram">The ModemTelegram to send.</param>
    public void Send(ManagementTelegram telegram)
    {
      this.sendQueue.Add(new SendManagementTelegram(telegram));
    }

    /// <summary>
    /// Disconnects the socket.
    /// </summary>
    public void Close()
    {
      Delegate[] eventHandlers;

      lock (this)
      {
        this.sendQueue.CompleteAdding();

        if (this.client != null)
        {
          if (this.client.Connected)
          {
            this.client.Shutdown(SocketShutdown.Both);
            this.client.Disconnect(false);
          }

          this.client.Close();
          this.client.Dispose();
          this.client = null;
        }

        // Remove event sinks to help GC.
        if (this.Initialized != null)
        {
          eventHandlers = this.Initialized.GetInvocationList();
          foreach (Delegate d in eventHandlers)
          {
            this.Initialized -= (EventHandler<EventArgs>)d;
          }
        }

        if (this.ResponseTimeout != null)
        {
          eventHandlers = this.ResponseTimeout.GetInvocationList();
          foreach (Delegate d in eventHandlers)
          {
            this.ResponseTimeout -= (EventHandler<ResponseTimeoutEventArgs>)d;
          }
        }

        if (this.TelegramReceived != null)
        {
          eventHandlers = this.TelegramReceived.GetInvocationList();
          foreach (Delegate d in eventHandlers)
          {
            this.TelegramReceived -= (EventHandler<TelegramReceivedEventArgs>)d;
          }
        }

        if (this.TransieverDisconnected != null)
        {
          eventHandlers = this.TransieverDisconnected.GetInvocationList();
          foreach (Delegate d in eventHandlers)
          {
            this.TransieverDisconnected -= (EventHandler<TransieverDisconnectedEventArgs>)d;
          }
        }

        if (this.UnknownTelegramReceived != null)
        {
          eventHandlers = this.UnknownTelegramReceived.GetInvocationList();
          foreach (Delegate d in eventHandlers)
          {
            this.UnknownTelegramReceived -= (EventHandler<UnknownDataReceivedEventArgs>)d;
          }
        }

        if (this.ManagementTelegramReceived != null)
        {
          eventHandlers = this.ManagementTelegramReceived.GetInvocationList();
          foreach (Delegate d in eventHandlers)
          {
            this.ManagementTelegramReceived -= (EventHandler<ManagementTelegramReceivedEventArgs>)d;
          }
        }
      }
    }

    /// <summary>
    /// Starts receiving and sends messages to initialize properties.
    /// </summary>
    internal void Initialize()
    {
      SocketAsyncEventArgs readArgs;
      bool pending;

      readArgs = new SocketAsyncEventArgs();
      readArgs.SetBuffer(this.receiveBuffer, 0, this.receiveBuffer.Length);
      readArgs.Completed += new EventHandler<SocketAsyncEventArgs>(this.ReadCompleted);

      pending = this.client.ReceiveAsync(readArgs);

      if (!pending)
      {
        this.ReadCompleted(this, readArgs);
      }

      this.Send(new ReadMode());
      this.Send(new ReadIdBase());
      this.Send(new ReadModemStatus());
      this.Send(new ReadRadioSensitivity());
      this.Send(new ReadSoftwareVersion());

      ThreadPool.QueueUserWorkItem(this.SendNextMessage);
    }

    /// <summary>
    /// Event handler for the SocketAsyncEventArgs.Completed event.
    /// This is where most of the receiving logic happens.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">A SocketAsyncEventArgs that contains the event data.</param>
    private void ReadCompleted(object sender, SocketAsyncEventArgs e)
    {
      bool pending;
      string cmd;
      Telegram t;
      ManagementTelegram m;
      byte channel;
      byte[] buffer;
      byte[] invalidData;

      if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
      {
        try
        {
          if (e.Offset + e.BytesTransferred < e.Buffer.Length)
          {
            e.SetBuffer(e.Offset + e.BytesTransferred, e.Buffer.Length - e.Offset - e.BytesTransferred);

            pending = this.client.ReceiveAsync(e);
            if (!pending)
            {
              this.ReadCompleted(this, e);
            }
          }
          else
          {
            cmd = Encoding.ASCII.GetString(e.Buffer, 0, e.Buffer.Length);

            // Management telegram
            if (cmd.StartsWith("A55A9") || cmd.StartsWith("A55B9"))
            {
              this.responseReceived.Set();
              m = ManagementTelegram.Parse(cmd);
              this.Intercept(m);
              ThreadPool.QueueUserWorkItem(state => this.ManagementTelegramReceived(this, new ManagementTelegramReceivedEventArgs(m)), null);
            }
            else
            {
              buffer = this.ConvertAsciiTelegramToBytes(cmd);

              // In normal mode the STC-Ethernet puts the channel in the checksum field
              if (this.OperatingMode == OperatingMode.Normal)
              {
                channel = buffer[13];

                // Calcualte the correct checksum
                buffer[13] = 0;
                for (int i = 2; i < 13; i++)
                {
                  buffer[13] += buffer[i];
                }
              }
              else
              {
                channel = 0xFF;
              }

              t = Telegram.Parse(buffer);

              if (t is MessageTelegram)
              {
                this.responseReceived.Set();
                this.Intercept((MessageTelegram)t);
              }

              ThreadPool.QueueUserWorkItem(state => this.TelegramReceived(this, new TelegramReceivedEventArgs(t, channel)), null);
            }

            if (!this.hasInitializedEventFired && this.ArePropertiesInitialized)
            {
              ThreadPool.QueueUserWorkItem(state => this.Initialized(this, new EventArgs()), null);
              this.hasInitializedEventFired = true;
            }

            e.SetBuffer(0, e.Buffer.Length);
            pending = this.client.ReceiveAsync(e);
            if (!pending)
            {
              this.ReadCompleted(this, e);
            }
          }
        }
        catch (Exception ex)
        {
          invalidData = new byte[e.Buffer.Length];
          Array.Copy(e.Buffer, invalidData, invalidData.Length);

          ThreadPool.QueueUserWorkItem(state => this.UnknownTelegramReceived(this, new UnknownDataReceivedEventArgs(ex, invalidData)), null);

          e.SetBuffer(0, e.Buffer.Length);
          pending = this.client.ReceiveAsync(e);
          if (!pending)
          {
            this.ReadCompleted(this, e);
          }
        }
      }
      else
      {
        this.TransieverDisconnected(this, new TransieverDisconnectedEventArgs((Transiever)this));
        this.Close();
      }
    }

    /// <summary>
    /// Converts a telegram represented in ASCII form to byte form.
    /// </summary>
    /// <param name="message">A telegram in ASCII from.</param>
    /// <returns>A telegram in byte form.</returns>
    private byte[] ConvertAsciiTelegramToBytes(string message)
    {
      byte[] buffer;

      if (string.IsNullOrEmpty(message))
      {
        throw new ArgumentNullException("message");
      }

      if (message.Length != 28)
      {
        throw new ArgumentOutOfRangeException("message", "Message should be 28 characters.");
      }

      buffer = new byte[14];
      for (int i = 0; i < 14; i++)
      {
        buffer[i] = byte.Parse(message.Substring(2 * i, 2), NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo);
      }

      return buffer;
    }

    /// <summary>
    /// Intercepts MessageTelegrams to set properties of this object.
    /// </summary>
    /// <param name="t">A MessageTelegram object.</param>
    private void Intercept(MessageTelegram t)
    {
      InformSoftwareVersion v;

      if (t is InformIdBase)
      {
        this.BaseId = ((InformIdBase)t).IdBase;
      }
      else if (t is InformModemStatus)
      {
        this.IsModemOn = ((InformModemStatus)t).IsModemOn;
        this.ModemId = ((InformModemStatus)t).ModemId;
      }
      else if (t is InformRadioSensitivity)
      {
        this.RadioSensitivity = ((InformRadioSensitivity)t).Sensitivity;
      }
      else if (t is InformSoftwareVersion)
      {
        v = (InformSoftwareVersion)t;
        this.SoftwareVersion = v.VersionPosition1 + "." + v.VersionPosition2 + "." + v.VersionPosition3 + "." + v.VersionPosition4;
      }
    }

    /// <summary>
    /// Intercepts ManagementTelegrams to set properties of this object.
    /// </summary>
    /// <param name="t">A ManagementTelegram object.</param>
    private void Intercept(ManagementTelegram t)
    {
      if (t is InformMode)
      {
        this.OperatingMode = ((InformMode)t).Mode;
      }
    }

    /// <summary>
    /// Sends the next message from the queue to the transiever. This method is called
    /// from a ThreadPool thread, hence the method signature.
    /// </summary>
    /// <param name="state">This parameter is not used.</param>
    private void SendNextMessage(object state)
    {
      SendTelegram message = null;
      Telegram enocean;
      ManagementTelegram management;
      bool signaled;

      while (!this.sendQueue.IsCompleted)
      {
        try
        {
          signaled = this.responseReceived.WaitOne(Constants.ResponseTimeout);

          // Timed out
          if (!signaled)
          {
            this.responseReceived.Set();
            ThreadPool.QueueUserWorkItem(s => this.ResponseTimeout(this, new ResponseTimeoutEventArgs((Telegram)s)), ((SendEnOceanTelegram)message).Telegram);
          }

          message = this.sendQueue.Take();

          if (this.client != null)
          {
            if (message is SendEnOceanTelegram)
            {
              enocean = ((SendEnOceanTelegram)message).Telegram;

              if (enocean is Wake)
              {
                this.responseReceived.Set();
                enocean = new ReadSoftwareVersion(); // STC-Ethernet does not support wake messages
              }
              else if (enocean is Sleep)
              {
                this.responseReceived.Set();
              }
              else
              {
                this.responseReceived.Reset();
              }

              enocean.GetBytes(this.sendBuffer);
              this.ConvertToAscii(this.sendBuffer);
              this.client.Send(this.sendBuffer);
            }
            else
            {
              management = ((SendManagementTelegram)message).Telegram;

              if (management is SetMode || management is ReadMode)
              {
                this.responseReceived.Reset();
              }
              else
              {
                this.responseReceived.Set();
              }

              management.GetBytes(this.managementSendBuffer);
              this.ConvertToAscii(this.managementSendBuffer);
              this.client.Send(this.managementSendBuffer);
            }
          }
        }
        catch (Exception)
        {
        }
      }
    }

    /// <summary>
    /// Converts a message in byte from to ASCII form. The data are replaced in the byte array.
    /// </summary>
    /// <param name="message">The message to convert.</param>
    private void ConvertToAscii(byte[] message)
    {
      for (int i = (message.Length / 2) - 1; i >= 0; i--)
      {
        message[(2 * i) + 1] = this.ConvertToAsciiChar((byte)(message[i] & 0xf));
        message[(2 * i)] = this.ConvertToAsciiChar((byte)(message[i] >> 4));
      }
    }

    /// <summary>
    /// Converts a 4 bit number to the ASCII representation.
    /// </summary>
    /// <param name="value">A value in the  range 0x0 to 0xF inclusive.</param>
    /// <returns>The ASCII value representing the number.</returns>
    private byte ConvertToAsciiChar(byte value)
    {
      if (value >= 0x0 && value <= 0x9)
      {
        return (byte)(value + 0x30);
      }
      else if (value >= 0xA && value <= 0XF)
      {
        return (byte)(value + 0x37);
      }
      else
      {
        throw new ArgumentOutOfRangeException("value", "must be in the range 0x0 to 0xF inclusive");
      }
    }
  }
}
